// Written in the D Programming Language
/**
 * The 11th lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=11">NeHe tutorial series</a>.
 * Originally written by Bosco and Jeff Molofee.
 *
 * Authors: Bosco
 *          Jeff Molofee
 *          Olli Aalto
 */
module lesson11;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;

import tango.stdc.stringz;
import Math = tango.math.Math;

/// The window title
const char[] WINDOW_TITLE = "Bosco & NeHe's Waving Texture Tutorial (D version)";

/// The main loop flag
bool running = true;

/// X Rotation
GLfloat xrot = 0.0f;
/// Y Rotation
GLfloat yrot = 0.0f;
/// Z Rotation
GLfloat zrot = 0.0f;

/// Storage For One Texture
GLuint texture;

/// The Array For The Points On The Grid Of Our "Wave"
GLfloat[][][] points;
/// Counter Used To Control How Fast Flag Waves
GLint wiggleCount;

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if (SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;

	if (args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen);
	initGL();

	while (running)
	{
		processEvents();
		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);
	}
}

/**
 * function to load in bitmap as a GL texture.
 */
void loadGLTextures()
{
	// Create storage space for the texture
	SDL_Surface *textureImage;

	// Load The Bitmap
	if ((textureImage = SDL_LoadBMP("data/Tim.bmp")) !is null)
	{
		// Free the surface when exiting the scope
		scope(exit) SDL_FreeSurface(textureImage);

		// Create The Texture
		glGenTextures(1, &texture);

		// Typical Texture Generation Using Data From The Bitmap
		glBindTexture(GL_TEXTURE_2D, texture);

		// Linear Filtering
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		// Generate The Texture
		glTexImage2D(GL_TEXTURE_2D, 0, 3, textureImage.w, textureImage.h, 0, GL_BGR,
					 GL_UNSIGNED_BYTE, textureImage.pixels);

		return;
	}
	throw new Exception("Failed to load texture.");
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}
}

/**
 * Process a key released event.
 */
void keyReleased(int key)
{
	switch (key)
	{
		case SDLK_ESCAPE:
			running = false;
			break;
		default:
			break;
	}
}

/**
 * Resize And Initialize The GL Window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if (height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);
	// Reset The Projection Matrix
	glLoadIdentity();

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f, 100.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);
	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * All Setup For OpenGL Goes Here.
 */
void initGL()
{
	loadGLTextures();

	// Enable Texture Mapping
	glEnable(GL_TEXTURE_2D);
	// Enables Smooth Shading
	glShadeModel(GL_SMOOTH);
	// Black Background
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	// Depth Buffer Setup
	glClearDepth(1.0f);
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
	// The Type Of Depth Test To Do
	glDepthFunc(GL_LEQUAL);
	// Really Nice Perspective Calculations
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	// Change to texture matrix and flip and rotate the texture
	glMatrixMode(GL_TEXTURE);
	glRotatef(180.0f, 0.0f, 0.0f, 1.0f);
	glScalef(-1.0f, 1.0f, 1.0f);
	// Back to normal
	glMatrixMode(GL_MODELVIEW);

	// Back face is filled in
	glPolygonMode(GL_BACK, GL_FILL);
	// Front face is drawn with lines
	glPolygonMode(GL_FRONT, GL_LINE);

	// Nice and simple way to initialize a multi-dimensional array
	points = new GLfloat[][][] (45, 45, 3);
	// Loop Through The X Plane
	for (int x = 0; x < 45; x++)
	{
		// Loop Through The Y Plane
		for (int y = 0; y < 45; y++)
		{
			// Apply The Wave To Our Mesh
			points[x][y][0] = cast(float) ((x / 5.0f) - 4.5f);
			points[x][y][1] = cast(float) ((y / 5.0f) - 4.5f);
			points[x][y][2] = cast(float) (Math.sin((((x / 5.0f) * 40.0f) / 360.0f) * Math.PI * 2.0f));
		}
	}
}

/**
 * Here's Where We Do All The Drawing.
 */
void drawGLScene()
{
	// Clear The Screen And Depth Buffer
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	// Reset The Current Matrix
	glLoadIdentity();

	// Translate 12 Units Into The Screen
	glTranslatef(0.0f, 0.0f, -12.0f);

	// Rotate On The X Axis
	glRotatef(xrot, 1.0f, 0.0f, 0.0f);
	// Rotate On The Y Axis
	glRotatef(yrot, 0.0f, 1.0f, 0.0f);
	// Rotate On The Z Axis
	glRotatef(zrot, 0.0f, 0.0f, 1.0f);

	// Select Our Texture
	glBindTexture(GL_TEXTURE_2D, texture);

	// Start Drawing Our Quads
	glBegin(GL_QUADS);
	// Loop Through The X Plane (44 Points)
	for (int i = 0; i < 44; i++)
	{
		// Loop Through The Y Plane (44 Points)
		for (int j = 0; j < 44; j++)
		{
			// Create A Floating Point X Value
			float x = i / 44.0f;
			// Create A Floating Point Y Value
			float y = j / 44.0f;
			// Create A Floating Point Y Value + 0.0227f
			float xb = (i + 1) / 44.0f;
			// Create A Floating Point Y Value + 0.0227f
			float yb = (j + 1) / 44.0f;

			// First Texture Coordinate (Bottom Left)
			glTexCoord2f(x, y);
			glVertex3f(points[i][j][0], points[i][j][1], points[i][j][2]);

			// Second Texture Coordinate (Top Left)
			glTexCoord2f(x, yb);
			glVertex3f(points[i][j + 1][0], points[i][j + 1][1], points[i][j + 1][2]);

			// Third Texture Coordinate (Top Right)
			glTexCoord2f(xb, yb);
			glVertex3f(points[i + 1][j + 1][0], points[i + 1][j + 1][1], points[i + 1][j + 1][2]);

			// Fourth Texture Coordinate (Bottom Right)
			glTexCoord2f(xb, y);
			glVertex3f(points[i + 1][j][0], points[i + 1][j][1], points[i + 1][j][2]);
		}
	}
	// Done Drawing Our Quads
	glEnd();

	// Used To Slow Down The Wave (Every 2nd Frame Only)
	if (wiggleCount == 2)
	{
		// Loop Through The Y Plane
		for (int y = 0; y < 45; y++)
		{
			// Store Current Value One Left Side Of Wave
			float hold = points[0][y][2];
			// Loop Through The X Plane
			for (int x = 0; x < 44; x++)
			{
				// Current Wave Value Equals Value To The Right
				points[x][y][2] = points[x + 1][y][2];
			}
			// Last Value Becomes The Far Left Stored Value
			points[$ - 1][y][2] = hold;
		}
		// Set Counter Back To Zero
		wiggleCount = 0;
	}
	// Increase The Counter
	wiggleCount++;

	// Increase The X Rotation Variable
	xrot += 0.3f;
	// Increase The Y Rotation Variable
	yrot += 0.2f;
	// Increase The Z Rotation Variable
	zrot += 0.4f;
}

void createGLWindow(char[] title, int width, int height, int bits, bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	/* Note the SDL_DOUBLEBUF flag is not required to enable double
	 * buffering when setting an OpenGL video mode.
	 * Double buffering is enabled or disabled using the
	 * SDL_GL_DOUBLEBUFFER attribute.
	 */
	int mode = SDL_OPENGL;
	if (fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if (SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
